﻿namespace CalculatorLibrary
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;
    using System.Text;
    using System.Text.RegularExpressions;

    /// <summary>
    ///     Class Calculator
    /// </summary>
    public class Calculator
    {
        #region Constants

        /// <summary>
        ///     The exit constant
        /// </summary>
        private const string ExitConstant = "/exit";

        /// <summary>
        ///     The help constant
        /// </summary>
        private const string HelpConstant = "/help";

        /// <summary>
        ///     The input constant
        /// </summary>
        private const string InputConstant = "/input";

        /// <summary>
        ///     The left number group
        /// </summary>
        private const string LeftNumberGroup = "LeftNumber";

        /// <summary>
        ///     The operator group
        /// </summary>
        private const string OperatorGroup = "Operator";

        /// <summary>
        ///     The right number group
        /// </summary>
        private const string RightNumberGroup = "RightNumber";

        #endregion

        #region Static Fields

        /// <summary>
        ///     The allowed complex operators
        /// </summary>
        private static readonly List<string> AllowedComplexOperators;

        /// <summary>
        ///     The allowed operands
        /// </summary>
        private static readonly List<char> AllowedOperators;

        /// <summary>
        ///     The allowed chars
        /// </summary>
        private static readonly string DecimalDelimiter;

        /// <summary>
        ///     The regular expression
        /// </summary>
        private static readonly string RegularExpression;

        #endregion

        #region Constructors and Destructors

        /// <summary>
        ///     Initializes static members of the <see cref="Calculator" /> class.
        /// </summary>
        static Calculator()
        {
            NumberFormatInfo nfi = CultureInfo.CurrentCulture.NumberFormat;
            var decimalSeparator = nfi.NumberDecimalSeparator;
            DecimalDelimiter = decimalSeparator;
            AllowedOperators = new List<char> { '+', '-' };
            AllowedComplexOperators = new List<string> { "modSuperPuper" };

            /*RegularExpression = @"^(?<LeftNumber>(\+|\-)?\d+((\.)\d+)?)(?<Operator>[+-]|modSuperPuper)(?<RightNumber>(\+|\-)?\d+((\.)\d+)?)$";*/
            RegularExpression = GenerateRegularExpression();
        }

        #endregion

        #region Public Methods and Operators

        /// <summary>
        ///     Calculates the specified input string.
        /// </summary>
        /// <param name="inputString">
        ///     The input string.
        /// </param>
        /// <returns>
        ///     System.String.
        /// </returns>
        public CalculationResult Calculate(string inputString)
        {
            ReturnState state;
            string stringResult;
            var constantKeysCheckResult = CheckInputForConstantKeys(inputString, out state, out stringResult);
            if (constantKeysCheckResult)
            {
                return new CalculationResult
                           {
                               IsError = false, 
                               Result = null, 
                               ReturnState = state, 
                               StringResult = stringResult
                           };
            }

            string errorString;
            if (!ValidateInput(inputString, out errorString))
            {
                return new CalculationResult
                           {
                               IsError = true, 
                               Result = null, 
                               ReturnState = ReturnState.Continue, 
                               StringResult = errorString
                           };
            }

            try
            {
                decimal resultOfCalculation = ProcessData(inputString);
                return new CalculationResult
                           {
                               IsError = false, 
                               Result = resultOfCalculation, 
                               ReturnState = ReturnState.Continue, 
                               StringResult = string.Empty
                           };
            }
            catch (Exception ex)
            {
                return new CalculationResult
                           {
                               IsError = true, 
                               Result = null, 
                               ReturnState = ReturnState.Continue, 
                               StringResult = string.Format("Unexpected exception: {0}", ex.Message)
                           };
            }
        }

        /// <summary>
        ///     Gets the usage help.
        /// </summary>
        /// <returns>System.String.</returns>
        public string GetUsageHelp()
        {
            var sb = new StringBuilder();
            sb.AppendLine("Usage:");
            sb.AppendLine(string.Concat(HelpConstant, " - shows this text block"));
            sb.AppendLine(string.Concat(InputConstant, " - shows allowed input format"));
            sb.AppendLine(string.Concat(ExitConstant, " - terminates calculation"));
            sb.AppendLine("-----------------------------------");
            return sb.ToString();
        }

        #endregion

        #region Methods

        /// <summary>
        /// Generates the regular expression.
        /// </summary>
        /// <returns>System.String.</returns>
        private static string GenerateRegularExpression()
        {
            var sb = new StringBuilder();
            var leftPart = string.Format(@"(?<{0}>(\+|\-)?\d+((\{1})\d+)?)", LeftNumberGroup, DecimalDelimiter);
            var simpleOperatorsString = string.Join(string.Empty, AllowedOperators);
            var complexOperatorsString = string.Empty;
            AllowedComplexOperators.ForEach(o => complexOperatorsString = string.Concat(complexOperatorsString, "|", o));
            var operatorString = string.Format(
                @"(?<{0}>[{1}]{2})", 
                OperatorGroup, 
                simpleOperatorsString, 
                complexOperatorsString);
            var rightPart = string.Format(@"(?<{0}>(\+|\-)?\d+((\{1})\d+)?)", RightNumberGroup, DecimalDelimiter);
            sb.Append("^");
            sb.Append(leftPart);
            sb.Append(operatorString);
            sb.Append(rightPart);
            sb.Append("$");
            return sb.ToString();
        }

        /// <summary>
        ///     Checks the input for constant keys.
        /// </summary>
        /// <param name="inputString">
        ///     The input string.
        /// </param>
        /// <param name="returnState">
        ///     State of the return.
        /// </param>
        /// <param name="resultOrError">
        ///     The result or error.
        /// </param>
        /// <returns>
        ///     <c>true</c> if XXXX, <c>false</c> otherwise
        /// </returns>
        private bool CheckInputForConstantKeys(
            string inputString, 
            out ReturnState returnState, 
            out string resultOrError)
        {
            switch (inputString)
            {
                case HelpConstant:
                    {
                        resultOrError = GetUsageHelp();
                        returnState = ReturnState.Continue;
                        return true;
                    }

                case InputConstant:
                    {
                        resultOrError = GetInputFormat();
                        returnState = ReturnState.Continue;
                        return true;
                    }

                case ExitConstant:
                    {
                        resultOrError = string.Format("Calculation terminated.");
                        returnState = ReturnState.Terminate;
                        return true;
                    }

                default:
                    {
                        resultOrError = string.Empty;
                        returnState = ReturnState.NA;
                        return false;
                    }
            }
        }

        /// <summary>
        ///     Gets the allowed operators string.
        /// </summary>
        /// <returns>System.String.</returns>
        private string GetAllowedOperatorsString()
        {
            var sb = new StringBuilder();
            var allOperators =
                AllowedComplexOperators.Concat(AllowedOperators.Select(c => c.ToString(CultureInfo.InvariantCulture)))
                    .ToList();
            for (var i = 0; i < allOperators.Count; i++)
            {
                if (i != 0)
                {
                    sb.Append(" ");
                }

                sb.Append(string.Format("'{0}'", allOperators[i]));
            }

            return sb.ToString();
        }

        /// <summary>
        ///     Gets the input format.
        /// </summary>
        /// <returns>System.String.</returns>
        private string GetInputFormat()
        {
            var sb = new StringBuilder();
            sb.AppendLine("Each expression should contain three parts:");
            const string NumbersString = "Any number preceded or not with sign character (+ or -)";
            sb.AppendLine(string.Format("\t1) {0}", NumbersString));
            sb.Append("\t2) One operator of the following list: ");
            sb.AppendLine(GetAllowedOperatorsString());
            sb.AppendLine(string.Format("\t3) {0}", NumbersString));
            sb.Append(
                string.Format("* As a delimiter of decimal part of a number should be used '{0}'", DecimalDelimiter));
            return sb.ToString();
        }

        /// <summary>
        ///     Determines whether [is digits only] [the specified STR].
        /// </summary>
        /// <param name="str">
        ///     The STR.
        /// </param>
        /// <returns>
        ///     <c>true</c> if [is digits only] [the specified STR]; otherwise, <c>false</c>.
        /// </returns>
        private bool IsDigitsOnly(string str)
        {
            foreach (char c in str)
            {
                if (c < '0' || c > '9')
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        ///     Parses the operand.
        /// </summary>
        /// <param name="input">
        ///     The input.
        /// </param>
        /// <returns>
        ///     System.Decimal.
        /// </returns>
        private decimal ParseOperand(string input)
        {
            if (IsDigitsOnly(input))
            {
                return int.Parse(input);
            }

            decimal value;
            decimal.TryParse(input, out value);
            return value;
        }

        /// <summary>
        ///     The process data.
        /// </summary>
        /// <param name="inputString">
        ///     The input string.
        /// </param>
        /// <returns>
        ///     The <see cref="string" />.
        /// </returns>
        private decimal ProcessData(string inputString)
        {
            var regex = new Regex(RegularExpression);
            var groupCollection = regex.Match(inputString).Groups;
            var leftNumber = ParseOperand(groupCollection[LeftNumberGroup].Value);
            var operatorString = groupCollection[OperatorGroup].Value;
            var rightNumber = ParseOperand(groupCollection[RightNumberGroup].Value);
            var calculationProvider = GetCalculationProvider(operatorString);

            return calculationProvider.Calculate(leftNumber, rightNumber);
        }

        /// <summary>
        ///     Validates the input.
        /// </summary>
        /// <param name="inputString">
        ///     The input string.
        /// </param>
        /// <param name="errorString">
        ///     The error string.
        /// </param>
        /// <returns>
        ///     <c>true</c> if XXXX, <c>false</c> otherwise
        /// </returns>
        private bool ValidateInput(string inputString, out string errorString)
        {
            if (string.IsNullOrEmpty(inputString))
            {
                errorString = "Can't process empty input.";
                return false;
            }

            var regex = new Regex(RegularExpression);
            if (!regex.IsMatch(inputString))
            {
                errorString = string.Format("Expression is not valid. Use {0} to get input rules.", InputConstant);
                return false;
            }

            errorString = string.Empty;
            return true;
        }

        #endregion
    }
}